﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Collections.Concurrent;
using System.ComponentModel;
using Framework.Extensions;

namespace Framework.Data
{
    public class Table<T> : View<T>, ITable<T> where T : new()
    {
        public IEnumerable<string> ReadOnlyFields { get; private set; }

        protected IEnumerable<string> NonReadOnlyKeys
        {
            get
            {
                return Keys.Where(x => !ReadOnlyFields.Contains(x));
            }
        }

        public Table(IDatabase db, string tableName)
            : this(db, tableName, null, null)
        {
        }

        public Table(IDatabase db)
            : this(db, null, null, null)
        {
        }

        public Table(IDatabase db, string tableName, string primaryKeyField)
            : this(db, tableName, primaryKeyField, null)
        {
        }

        public Table(IDatabase db, string tableName, string primaryKeyField, string readOnlyFields)
            : base(db, tableName, primaryKeyField)
        {
            ReadOnlyFields = !String.IsNullOrWhiteSpace(readOnlyFields) ? readOnlyFields.Split(',').Select(x => x.Trim()).ToArray() : RuntimeDetectionOfReadOnlyFields();
        }

        private IEnumerable<string> RuntimeDetectionOfReadOnlyFields()
        {
            return typeof(T)
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(x => x.GetCustomAttributes(typeof(ReadOnlyAttribute), true).Cast<ReadOnlyAttribute>().Select(y => y.IsReadOnly).FirstOrDefault())
                .Select(x => x.Name);
        }

        public void Save(T o)
        {
            var properties = o.ToDictionary(true && !o.GetType().IsAnonymous());

            if (!(properties.Count > 0))
                throw new InvalidOperationException("Can't parse this object to the database - there are no properties updateSet");

            if (Keys.Any(k => !properties.Keys.Contains(k)))
                throw new InvalidOperationException("Object does not have all defined keys in dynamic table constructor.");

            var props = o.GetType()
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(x => x.GetCustomAttributes(typeof(ReadOnlyAttribute), true).Cast<ReadOnlyAttribute>().Select(y => y.IsReadOnly).FirstOrDefault())
                .Select(x => x.Name);

            ReadOnlyFields = ReadOnlyFields.Concat(props);

            if (ReadOnlyFields != null)
            {
                var toRemove = properties.Where(y => ReadOnlyFields.Contains(y.Key)).Select(x => x.Key).ToList();
                toRemove.ForEach(y => properties.Remove(y));
            }

            var sql = "";

            if (ImplementationDetails.SupportsUpsertStatement)
            {
                sql = ImplementationDetails.GetUpsertStatement(DatabaseObjectName, Keys, properties.Keys);
            }
            var values = properties.Where(x => Keys.Contains(x.Key)).Select(x => x.Value)
                    .Concat(properties.Where(x => !Keys.Contains(x.Key)).Select(x => x.Value));

            base.NonQuery(sql, values.ToArray());
        }

        public void Delete(object o)
        {
            if (Keys.Count() == 0)
            {
                throw new InvalidOperationException("Can't automate delete for type " + o.GetType().Name + " - it has no defined primary keys");
            }
            var keys = GetKeysProperties(o);

            List<object> arguments = new List<object>();
            var sbWhere = new List<string>();
            int counter = 0;
            foreach (var item in keys)
            {
                sbWhere.Add(string.Format("{0} = @{1}", item.Key, counter.ToString()));
                arguments.Add(item.Value);
                counter++;
            }

            var where = string.Join(" AND ", sbWhere.ToArray());
            DeleteMany(where, arguments.ToArray());
        }

        public void DeleteMany(string where = "", params object[] args)
        {
            string sql = ImplementationDetails.GetDeleteStatement(DatabaseObjectName, where);
            base.NonQuery(sql, args);
        }

        public void DeleteAll()
        {
            DeleteMany("", null);
        }

        public void Update(T o) 
        {
            if (Keys.Count() == 0)
                throw new InvalidOperationException("Can't automate update for type " + typeof(T).Name + " - it has no defined primary keys");

            var keys = GetKeysProperties(o);
            if (Keys.Any(k => !keys.Keys.Contains(k)))
                throw new InvalidOperationException("Can't automate update for type " + typeof(T).Name + " - Some properties that have been declared as Keys for " + DatabaseObjectName + " and are not present.");

            var allProps = GetEditableProperties(o);
            if (!(allProps.Count > 0))
                throw new InvalidOperationException("Can't automate update for type " + typeof(T).Name + " - it has no properties (at least no editable properties).");

            var columnNames = allProps.Keys.ToList();

            var sql = ImplementationDetails.GetUpdateStatement(DatabaseObjectName, NonReadOnlyKeys, columnNames);
            List<object> args = keys.Values.ToList();
            args.AddRange(allProps.Values.ToArray());
            base.NonQuery(sql, args.ToArray());

        }

        public void Insert(T o) 
        {
            var keys = GetKeysProperties(o);

            if (NonReadOnlyKeys.Any(k => !keys.Keys.Contains(k)))
                throw new InvalidOperationException("Can't automate insert for type " + typeof(T).Name + " - Some properties that have been declared as Keys for " + DatabaseObjectName + " and are not present.");

            var allProps = GetEditableProperties(o);

            if (!(allProps.Count > 0))
                throw new InvalidOperationException("Can't automate insert for type " + typeof(T).Name + " - it has no properties (at least no editable properties).");

            var columnNames = allProps.Keys.ToList();

            var sql = ImplementationDetails.GetInsertStatement(DatabaseObjectName, columnNames);
            var args = allProps.Values.ToArray();
            base.NonQuery(sql, args);

        }

        IDictionary<string, object> GetKeysProperties(object o)
        {
            var properties = o.ToDictionary();
            return properties.Where(k => Keys.Contains(k.Key)).ToDictionary(x => x.Key, x => x.Value);
        }

        static ConcurrentDictionary<Type, List<string>> ReadOnlyPropertiesCache = new ConcurrentDictionary<Type, List<string>>();

        List<string> AllReadOnlyProperies(object o)
        {
            var objType = o.GetType();

            return ReadOnlyPropertiesCache.GetOrAdd(o.GetType(), (newType) =>
            {
                var withReadOnlyAttribute = objType
                    .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(x => x.GetCustomAttributes(typeof(ReadOnlyAttribute), true).Cast<ReadOnlyAttribute>().Select(y => y.IsReadOnly).FirstOrDefault())
                    .Select(x => x.Name)
                    ;

                return ReadOnlyFields.Concat(withReadOnlyAttribute).ToList();
            });
        }

        static ConcurrentDictionary<Type, List<string>> EditablePropertiesCache = new ConcurrentDictionary<Type, List<string>>();
        List<string> AllEditableProperties(object o)
        {
            var objType = o.GetType();

            return EditablePropertiesCache.GetOrAdd(objType, (newType) =>
            {
                var properties = o.ToDictionary(true && !objType.IsAnonymous()).Select(x => x.Key).ToList();
                var readOnlyProps = AllReadOnlyProperies(o);
                var toRemove = properties.Where(y => ReadOnlyFields.Contains(y)).ToList();
                toRemove.ForEach(y => properties.Remove(y));
                return properties;
            });
        }

        IDictionary<string, object> GetEditableProperties(object o)
        {
            var objType = o.GetType();
            var keyValues = o.ToDictionary(true && !objType.IsAnonymous());

            AllReadOnlyProperies(o).ForEach(y => keyValues.Remove(y));
            return keyValues;
        }

        public void InsertMany(IEnumerable<T> list)
        {
            throw new NotImplementedException();
        }

        public void Execute(string sql, params object[] args)
        {
            base.NonQuery(sql, args);
        }
    }
}
